{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 使用字符级RNN生成名称\n",
    "\n",
    "## 概述\n",
    "\n",
    "在NLP的教程[《使用字符级RNN分类名称》](https://www.mindspore.cn/tutorial/zh-CN/master/middleclass/text/rnn_classification.html)中，我们使用了RNN训练神经网络将人名与对应国家进行分类。\n",
    "\n",
    "本教程中，我们将通过反向操作来生成不同语言的名称。这里仍通过编写由线性层结构构建出的小型RNN网络模型来实现目标。此次与上一篇教程最大的区别在于，不是在输入名字中的所有字母来预测分类，而是输入一个分类类别，然后一次输出一个字母。这种用于预测字符来形成一个单词的方法通常称为“语言模型”。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备环节"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 配置环境"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本教程我们在Ascend环境下，使用PyNative模式运行实验。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mindspore import context\n",
    "context.set_context(mode=context.PYNATIVE_MODE, device_target=\"Ascend\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 准备数据\n",
    "\n",
    "下载[数据](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/middleclass/data.zip)，并将其提取到当前目录。\n",
    "\n",
    "(!!!介绍数据)\n",
    "\n",
    "有关此过程的更多详细信息，请参见[《使用字符级RNN分类名称》](https://www.mindspore.cn/tutorial/zh-CN/master/middleclass/text/rnn_classification.html)。 简而言之，数据集由纯文本文件`data/names/[Language].txt`构成，每行都有一个名称。我们将行拆分成一个数组，将Unicode转换为ASCII，最后得到一个字典`{language: [names ...]}`。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在Jupyter Notebook中执行如下命令下载数据集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "!wget -NP ./ https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/middleclass/data.zip\n",
    "!unzip ./data.zip"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "# categories: 18 ['Japanese', 'Chinese', 'Polish', 'Scottish', 'Vietnamese', 'Czech', 'Portuguese', 'Dutch', 'Greek', 'French', 'Arabic', 'Russian', 'Spanish', 'Korean', 'English', 'Italian', 'German', 'Irish']\n",
      "O'Neal\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "import glob\n",
    "import string\n",
    "import unicodedata\n",
    "from io import open\n",
    "\n",
    "all_letters = string.ascii_letters + \" .,;'-\"\n",
    "n_letters = len(all_letters) + 1\n",
    "\n",
    "def findFiles(path): return glob.glob(path)\n",
    "\n",
    "# 将Unicode字符串转换为纯ASCII\n",
    "def unicodeToAscii(s):\n",
    "    return ''.join(\n",
    "        c for c in unicodedata.normalize('NFD', s)\n",
    "        if unicodedata.category(c) != 'Mn'\n",
    "        and c in all_letters\n",
    "    )\n",
    "\n",
    "# 读取文件并切分语句\n",
    "def readLines(filename):\n",
    "    lines = open(filename, encoding='utf-8').read().strip().split('\\n')\n",
    "    return [unicodeToAscii(line) for line in lines]\n",
    "\n",
    "# 建立类别语句的字典同时保存类别的列表\n",
    "category_lines = {}\n",
    "all_categories = []\n",
    "for filename in findFiles('data/names/*.txt'):\n",
    "    category = os.path.splitext(os.path.basename(filename))[0]\n",
    "    all_categories.append(category)\n",
    "    lines = readLines(filename)\n",
    "    category_lines[category] = lines\n",
    "\n",
    "n_categories = len(all_categories)\n",
    "\n",
    "if n_categories == 0:\n",
    "    raise RuntimeError('Data not found. Make sure that you downloaded data set to the current directory.')\n",
    "\n",
    "print('# categories:', n_categories, all_categories)\n",
    "print(unicodeToAscii(\"O'Néàl\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 创建网络\n",
    "\n",
    "该网络基于[《使用字符级RNN分类名称》](https://www.mindspore.cn/tutorial/zh-CN/master/middleclass/text/rnn_classification.html)教程中的RNN网络进行了扩展，附加了一个与其他向量连接在一起的新参数向量。该变量与字母输入采用一样的one-hot编码。\n",
    "\n",
    "与上一个网络结构略有不同，为了有更好的效果，在`output combined`层之后我们又添加了一个线性层`o2o`。与此同时也新添加一个`dropout`层，根据[论文](https://arxiv.org/abs/1207.0580)以一定的概率（此处为0.1）将输入的部分随机归零。这一步骤通常用于模糊输入来防止过拟合。这里我们在网络末端利用这一特性来故意添加意外情况，以及样本的多样性。\n",
    "\n",
    "(!!!图中颜色代表什么，新增加的层用不以言的颜色表示，我看不出来区别)\n",
    "\n",
    "![](images/run2.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "import mindspore.nn as nn\n",
    "import mindspore.ops as ops\n",
    "\n",
    "class RNN(nn.Cell):\n",
    "    \"\"\"定义RNN网络\"\"\"\n",
    "    def __init__(self, input_size, hidden_size, output_size):\n",
    "        super(RNN, self).__init__()\n",
    "        self.hidden_size = hidden_size\n",
    "\n",
    "        self.i2h = nn.Dense(n_categories + input_size + hidden_size, hidden_size)\n",
    "        self.i2o = nn.Dense(n_categories + input_size + hidden_size, output_size)\n",
    "        self.o2o = nn.Dense(hidden_size + output_size, output_size)\n",
    "        self.dropout = nn.Dropout(0.1)\n",
    "        self.softmax = nn.LogSoftmax(axis=1)\n",
    "\n",
    "    def construct(self, category, input, hidden):\n",
    "        # 构建RNN网络结构\n",
    "        op = ops.Concat(axis=1)\n",
    "        input_combined = op((category, input, hidden))\n",
    "        hidden = self.i2h(input_combined)\n",
    "        output = self.i2o(input_combined)\n",
    "        output_combined = op((hidden, output))\n",
    "        output = self.o2o(output_combined)\n",
    "        output = self.dropout(output)\n",
    "        output = self.softmax(output)\n",
    "        return output, hidden\n",
    "\n",
    "    def initHidden(self):\n",
    "        return Tensor(np.zeros((1, self.hidden_size)),mstype.float32)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "### 准备训练\n",
    "\n",
    "首先，定义一个能获取随机对`(category，line)`的辅助函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "# 从列表中随机生成随即对\n",
    "def randomChoice(l):\n",
    "    return l[random.randint(0, len(l) - 1)]\n",
    "\n",
    "# 获取随机类并获取该类的随机行\n",
    "def randomTrainingPair():\n",
    "    category = randomChoice(all_categories)\n",
    "    line = randomChoice(category_lines[category])\n",
    "    return category, line"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对于训练集中的每个名称，该网络的输入为：`(category, current letter, hidden state)`，输出为：`(next letter, next hidden state)`。因此对于每个训练集，我们都需要`categoryTensor`（代表种类的one-hot向量），用于输入的`inputTensor`（由首字母到尾字母（不包括EOS）组成的one-hot矩阵）和用于输出的`targetTensor`（由第二个字母到尾字母（不包括EOS）组成的向量）。\n",
    "\n",
    "在每个timestep中，因为我们需要预测当前字母所对应的下一个字母，所以需要拆分连续字母来组成字母对。例如：对于`\"ABCD<EOS>\"`，我们将创建出`('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'EOS')`字母对。\n",
    "\n",
    "![](images/pair.png)\n",
    "\n",
    "我们在训练时会持续将`category`向量传输至网络中，该向量是大小为`<1 x n_categories>`的[one-hot向量](https://en.wikipedia.org/wiki/One-hot)。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "(!!!函数去掉Example，编程规范，函数全部用小写，单词之间用_间隔，别整大写，类才大写)\n",
    "\n",
    "# 代表category的one-hot向量\n",
    "def categoryTensor(category):\n",
    "    li = all_categories.index(category)\n",
    "    tensor = Tensor(np.zeros((1, n_categories)),mstype.float32)\n",
    "    tensor[0,li] = 1.0\n",
    "    return tensor\n",
    "\n",
    "# 定义输入为一个由首字母到尾字母（不包括EOS）组成的one-hot矩阵\n",
    "def inputTensor(line):\n",
    "    tensor = Tensor(np.zeros((len(line), 1, n_letters)), mstype.float32)\n",
    "    for li in range(len(line)):\n",
    "        letter = line[li]\n",
    "        tensor[li,0,all_letters.find(letter)] = 1.0\n",
    "    return tensor\n",
    "\n",
    "# 由第二个字母到尾字母(不包括EOS)组成的向量\n",
    "def targetTensor(line):\n",
    "    letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]\n",
    "    letter_indexes.append(n_letters - 1) # EOS\n",
    "    return Tensor(np.array(letter_indexes), mstype.int64)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了方便训练，我们将使用`randomTrainingExample`函数来获取随机对`(category，line)`，并将其转换为所需格式的`(category, input, target)`向量。\n",
    "\n",
    "(!!!函数去掉Example，编程规范，函数全部用小写，单词之间用_间隔，别整大写，类才大写)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def randomTrainingExample():\n",
    "    category, line = randomTrainingPair()\n",
    "    category_tensor = categoryTensor(category)\n",
    "    input_line_tensor = inputTensor(line)\n",
    "    target_line_tensor = targetTensor(line)\n",
    "    return category_tensor, input_line_tensor, target_line_tensor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 训练网络\n",
    "\n",
    "与分类模型依赖最后输出作为结果不同，这里我们在每一步都进行了预测，因此每一步都需要计算损失。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mindspore import ops\n",
    "from mindspore.nn.loss import Loss\n",
    "\n",
    "# 定义Loss计算\n",
    "class NLLLoss(Loss):\n",
    "    def __init__(self, reduction='mean'):\n",
    "        super(NLLLoss, self).__init__(reduction)\n",
    "        self.one_hot = ops.OneHot()\n",
    "        self.reduce_sum = ops.ReduceSum()\n",
    "        \n",
    "    (!!!编程规范)\n",
    "    def construct(self, logits, label):\n",
    "        label_one_hot = self.one_hot(label, ops.shape(logits)[-1], ops.ScalarToArray(1.0), ops.ScalarToArray(0.0))\n",
    "        loss = self.reduce_sum(-1.0 * logits * label_one_hot, (1,))\n",
    "        return self.get_loss(loss)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "解释"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "criterion = NLLLoss()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "解释"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mindspore import nn, ops, Tensor\n",
    "import numpy as np\n",
    "from mindspore import dtype as mstype\n",
    "\n",
    "class WithLossCellRnn(nn.Cell):\n",
    "    \"\"\"构建有损失计算的RNN网络\"\"\"\n",
    "    \n",
    "    def __init__(self, backbone,loss_fn):\n",
    "        super(WithLossCellRnn, self).__init__(auto_prefix=True)\n",
    "        self._backbone = backbone\n",
    "        self._loss_fn = loss_fn\n",
    "\n",
    "    def construct(self, category_tensor, input_line_tensor, hidden, target_line_tensor):\n",
    "        loss = 0\n",
    "        for i in range(input_line_tensor.shape[0]):\n",
    "            output, hidden = self._backbone(category_tensor, input_line_tensor[i], hidden)\n",
    "            l = self._loss_fn(output, target_line_tensor[i])\n",
    "            loss += l\n",
    "        return loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "解释"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "rnn_cf = RNN(n_letters, 128, n_letters)\n",
    "\n",
    "# 创建优化器和损失网络实例\n",
    "optimizer = nn.Momentum(filter(lambda x:x.requires_grad,rnn_cf.get_parameters()),0.0001,0.9)\n",
    "net_with_criterion = WithLossCellRnn(rnn_cf, criterion)\n",
    "net = nn.TrainOneStepCell(net_with_criterion, optimizer)\n",
    "net.set_train()\n",
    "\n",
    "# 定义训练方法\n",
    "def train(category_tensor,input_line_tensor, target_line_tensor):\n",
    "    new_shape = list(target_line_tensor.shape)\n",
    "    new_shape.append(1)\n",
    "    target_line_tensor = target_line_tensor.reshape(new_shape)\n",
    "    hidden = rnn_cf.initHidden()\n",
    "    loss = net(category_tensor, input_line_tensor, hidden,target_line_tensor)\n",
    "    (!!!解释)\n",
    "    for i in range(input_line_tensor.shape[0]):\n",
    "        output, hidden = rnn_cf(category_tensor, input_line_tensor[i], hidden)\n",
    "    return  output, loss / input_line_tensor.shape[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了跟踪在网络模型训练的过程中的耗时，这里添加了一个`timeSince`函数，方便我们持续看到训练的整个过程："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "import math\n",
    "\n",
    "# 定义可读时间回调字符串\n",
    "def timeSince(since):\n",
    "    now = time.time()\n",
    "    s = now - since\n",
    "    m = math.floor(s / 60)\n",
    "    s -= m * 60\n",
    "    return '%dm %ds' % (m, s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在训练过程中，每经过`print_every`个样本打印当前时间和损失值。同时，在`all_losses`中根据`plot_every`的设定值计算平均损失，以便于后面绘制训练的过程图像。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1m 50s (500 6%) 3.6736\n",
      "2m 55s (1000 13%) 3.7153\n",
      "4m 4s (1500 20%) 4.1140\n",
      "5m 12s (2000 26%) 4.0867\n",
      "6m 16s (2500 33%) 3.5248\n",
      "7m 21s (3000 40%) 3.9484\n",
      "8m 26s (3500 46%) 3.5863\n",
      "9m 30s (4000 53%) 3.8334\n",
      "10m 34s (4500 60%) 4.1381\n",
      "11m 38s (5000 66%) 4.1342\n",
      "12m 43s (5500 73%) 3.1236\n",
      "13m 48s (6000 80%) 3.8153\n",
      "15m 0s (6500 86%) 4.0366\n",
      "16m 3s (7000 93%) 4.0648\n",
      "17m 9s (7500 100%) 3.6050\n"
     ]
    }
   ],
   "source": [
    "n_iters = 7500\n",
    "print_every = 500\n",
    "plot_every = 100\n",
    "all_losses = []\n",
    "\n",
    "# 每经过100次迭代，就重置为0\n",
    "total_loss = 0 \n",
    "\n",
    "start = time.time()\n",
    "\n",
    "for iter in range(1, n_iters + 1):\n",
    "    output, loss = train(*randomTrainingExample())\n",
    "    total_loss += loss\n",
    "    if iter % print_every == 0:\n",
    "        print('%s (%d %d%%) %.4f'% (timeSince(start), iter, iter / n_iters * 100, loss.asnumpy())) \n",
    "    if iter % plot_every == 0:\n",
    "        all_losses.append((total_loss / plot_every).asnumpy())\n",
    "        total_loss = 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用`matplotlib.pyplot`绘制训练过程中损失函数的图像。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0x7fcc91a3f590>]"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "plt.figure()\n",
    "plt.plot(all_losses)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 验证模型\n",
    "\n",
    "在训练结束后，对获得的模型进行验证。这里，我们向网络中输入一个字母并推理得出下一个。将这一步的输出字母作为下一步的输入，重复直到EOS标记处。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Rrlk\n",
      "Usnaolena\n",
      "Srkeaaainiknnuoo\n",
      "Gtranarssir\n",
      "Eauasacgliratgiukra\n",
      "Rilasakaaagitnhrgoaus\n",
      "Senah\n",
      "Po\n",
      "Atainalliganiiaiohua\n",
      "Cmar\n",
      "He\n",
      "Iaaaere\n"
     ]
    }
   ],
   "source": [
    "max_length = 20\n",
    "\n",
    "# 根据类别与起始字母创建名称\n",
    "def sample(category, start_letter='A'):\n",
    "    category_tensor = categoryTensor(category)\n",
    "    input = inputTensor(start_letter)\n",
    "    hidden = rnn_cf.initHidden()\n",
    "    output_name = start_letter\n",
    "    for i in range(max_length):\n",
    "        output, hidden = rnn_cf(category_tensor, input[0], hidden)\n",
    "        topk = ops.TopK(sorted=True)\n",
    "        topv, topi = topk(output,1)\n",
    "        topi = topi[0,0]\n",
    "        if topi == n_letters - 1:\n",
    "            break\n",
    "        else:\n",
    "            letter = all_letters[topi]\n",
    "            output_name += letter\n",
    "        input = inputTensor(letter)\n",
    "\n",
    "    return output_name\n",
    "\n",
    "# 遍历提供的字母，得到输出名称\n",
    "def samples(category, start_letters='ABC'):\n",
    "    for start_letter in start_letters:\n",
    "        print(sample(category, start_letter))\n",
    "\n",
    "samples('Russian', 'RUS')\n",
    "samples('German', 'GER')\n",
    "samples('Spanish', 'SPA')\n",
    "samples('Chinese', 'CHI')\n",
    "\n",
    "(!!!打印出来的东西根本不知道是什么，你起码有个对应关系，输入是什么，输出是什么)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
